Skip to content

feat: remove react-query in favor of server actions#22

Merged
toto04 merged 22 commits intomainfrom
remove-react-query
Apr 13, 2026
Merged

feat: remove react-query in favor of server actions#22
toto04 merged 22 commits intomainfrom
remove-react-query

Conversation

@lorenzocorallo
Copy link
Copy Markdown
Member

No description provided.

@lorenzocorallo lorenzocorallo requested a review from toto04 April 12, 2026 23:59
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Warning

Rate limit exceeded

@lorenzocorallo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aab4627d-c657-43c2-8c0e-a0368f54060b

📥 Commits

Reviewing files that changed from the base of the PR and between e3a4eaa and 2139007.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • package.json
  • src/app/dashboard/(active)/account/telegram.tsx
  • src/app/dashboard/(active)/telegram/grants/delete-grant.tsx
  • src/app/dashboard/(active)/telegram/grants/new-grant.tsx
  • src/app/dashboard/(active)/telegram/groups/page.tsx
  • src/app/dashboard/(active)/telegram/user-details/add-role.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-audit-log.tsx
  • src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx
  • src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx
  • src/app/dashboard/(active)/telegram/user-details/page.tsx
  • src/app/dashboard/(active)/telegram/user-details/remove-role.tsx
  • src/components/spinner.tsx
  • src/server/actions/azure.ts
  • src/server/actions/grants.ts
  • src/server/actions/groups.ts
  • src/server/actions/users.ts
  • src/server/auth/index.ts

Walkthrough

Replaces client-side tRPC + React Query with server-side tRPC client and server actions; removes client TRPC/react-query infra; converts many pages/components to async server components or to call server actions for reads/mutations; updates types and component signatures to accept server-fetched data or callback hooks.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Removed @tanstack/react-query, @trpc/react-query, @trpc/tanstack-react-query.
Client TRPC infra removed
src/lib/trpc/client.tsx, src/lib/trpc/query-client.ts, src/lib/trpc/server.tsx
Deleted TRPC provider, QueryClient factory, SSR hydration/prefetch helpers and their exports.
Server TRPC client & types
src/server/trpc/index.tsx, src/server/trpc/types.tsx
Added server-only trpc client and new type aliases (TgGrant, TgUserRole, AzureMember).
Server actions added
src/server/actions/*
src/server/actions/users.ts, .../azure.ts, .../grants.ts, .../groups.ts, .../test.ts
Added "use server" action wrappers that call tRPC procedures for reads and mutations (users, grants, groups, azure members, test DB).
Root & dashboard layouts
src/app/layout.tsx, src/app/dashboard/layout.tsx
Removed TRPC/Hydrate providers from RootLayout; replaced query-client prefetch with direct server trpc calls in Admin/dashboard layout.
Azure members area
src/app/dashboard/(active)/azure/members/...
page.tsx, table.tsx, columns.tsx, create-assoc-member.tsx, set-assoc-number-dialog.tsx
Components/pages now accept server-fetched members and use server actions (getAzureMembers, createAzureMember, setAzureMemberNumber); switched types to AzureMember.
Telegram grants & user pages
src/app/dashboard/(active)/telegram/grants/..., .../user-list/page.tsx, .../user-details/page.tsx
Converted pages to async server components, fetch grants/users server-side, pass data into presentational components, and replace client mutations with server actions (createGrant, interruptGrant, getUserGrant, etc.).
Telegram groups
src/app/dashboard/(active)/telegram/groups/page.tsx, group-row.tsx, search-input.tsx
Groups page moved server-side; extracted client GroupRow and SearchInput; server-side search and setGroupHide server action used instead of client TRPC.
User details child components
src/app/.../user-details/*.tsx
Role/group/grant dialogs and cards switched from client TRPC/react-query to server actions and now accept callback props (onAdd, onDelete, onConfirm) for post-action refresh.
Account & onboarding
src/app/(auth)/onboarding/no-role/page.tsx, src/app/dashboard/(active)/account/telegram.tsx
Rewrote to server components/calls: use getServerSession() and getUserRoles() instead of client-side TRPC/react-query.
Components & utilities
src/components/user-select.tsx, src/components/data-table.tsx, src/lib/utils/telegram.ts, src/app/testtrpc/page.tsx
user-select now calls server actions for lookups; data-table removed optional chaining on rows; updated type import paths; test page uses testDb() server action.
Middleware
src/middleware.ts
Narrowed matcher from /api/:path* to /api/auth/:path* and updated path checks to use AUTH_PATH.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (React UI)
  participant Action as Server Action (app/server/actions)
  participant TRPC as Server tRPC Client (src/server/trpc)
  participant Backend as Backend Procedure/DB
  Client->>Action: invoke server action (e.g. createGrant(input))
  Note right of Action: "use server" async function
  Action->>TRPC: trpc.tg.grants.create.mutate(input)
  TRPC->>Backend: call backend procedure / DB
  Backend-->>TRPC: return result
  TRPC-->>Action: return result
  Action-->>Client: return result (UI refresh / render)
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing React Query integration throughout the codebase in favor of server actions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/app/dashboard/(active)/telegram/grants/new-grant.tsx (1)

79-94: ⚠️ Potential issue | 🟠 Major

Guard the server-action call with try/catch.

Line 79 assumes a structured response, but createGrant(...) can throw. Add try/catch to prevent unhandled client-side failures.

Proposed fix
 async function create() {
   if (!user || !startDate || !endDate || !adderId) return
-  const res = await createGrant({
-    userId: user.id,
-    adderId,
-    since: startDate,
-    until: endDate,
-    reason: reason || undefined,
-    sendTgLog: true,
-  })
-
-  if (res.error === "UNAUTHORIZED") toast.error("You don't have permission to create grants.")
-  else if (res.error === "ALREADY_EXISTING") toast.error("This user already has an ongoing grant.")
-  else if (res.error === "INTERNAL_SERVER_ERROR") toast.error("There was a server error.")
-  else {
-    toast.success("Grant created successfully!")
-    router.refresh()
-  }
+  try {
+    const res = await createGrant({
+      userId: user.id,
+      adderId,
+      since: startDate,
+      until: endDate,
+      reason: reason || undefined,
+      sendTgLog: true,
+    })
+
+    if (res.error === "UNAUTHORIZED") toast.error("You don't have permission to create grants.")
+    else if (res.error === "ALREADY_EXISTING") toast.error("This user already has an ongoing grant.")
+    else if (res.error === "INTERNAL_SERVER_ERROR") toast.error("There was a server error.")
+    else {
+      toast.success("Grant created successfully!")
+      router.refresh()
+    }
+  } catch {
+    toast.error("There was a server error.")
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/grants/new-grant.tsx around lines 79 -
94, The createGrant(...) server-action call may throw and is currently
unguarded; wrap the await createGrant({...}) call in a try/catch around the
block that handles res so that thrown errors are caught and reported to the user
instead of causing an unhandled rejection. Specifically, place the await
createGrant call (and the subsequent res.error checks and success handling)
inside a try block, catch any error in the catch block, log or toast a
generic/error message (e.g., toast.error with the exception message) and ensure
router.refresh() and success toast only run in the happy path; reference the
createGrant invocation and the existing res/error branches when updating the
code.
src/components/user-select.tsx (1)

25-31: ⚠️ Potential issue | 🟡 Minor

Add validation for ID parsing to handle non-numeric input.

parseInt(id, 10) returns NaN for non-numeric input (e.g., empty string or text). This NaN value would be passed to getUserInfo, potentially causing unexpected server behavior.

🛡️ Proposed fix
 async function search() {
+  if (searchType === "id") {
+    const parsedId = parseInt(id, 10)
+    if (Number.isNaN(parsedId)) return toast.error("Please enter a valid numeric ID")
+    const user = await getUserInfo(parsedId)
+    if (!user) return toast.error("User with this ID not found")
+    setUser(user)
+    onUser(user)
+    return
+  }
+  const user = await searchUserInfo(username)
-  const user = searchType === "username" ? await searchUserInfo(username) : await getUserInfo(parseInt(id, 10))
   if (!user) return toast.error(`User with this ${searchType === "id" ? "ID" : "username"} not found`)
   setUser(user)
   onUser(user)
   return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/user-select.tsx` around lines 25 - 31, In search(), validate
the id before calling getUserInfo: check that the id string is non-empty and
parses to a finite integer (e.g., via /^\d+$/ test or
Number.isInteger(Number(id))) when searchType === "id"; if validation fails,
call toast.error with a clear message and return early so you never pass NaN to
getUserInfo; keep existing flow for username branch and continue to call
setUser(user) and onUser(user) only after a successful getUserInfo or
searchUserInfo result.
src/app/dashboard/(active)/azure/members/set-assoc-number-dialog.tsx (1)

35-41: ⚠️ Potential issue | 🟡 Minor

Keep the dialog open until the mutation succeeds.

handleOpenChange(false) runs before res.error is checked. On a failed save, the dialog closes, the typed value is cleared, and the user is left in a confusing loading/error state. Close/reset only after the success path.

Proposed fix
     const loading = toast.loading(`Updating...`)
     const res = await setAzureMemberNumber({ userId, assocNumber: parseInt(value, 10) })
-    handleOpenChange(false)
     if (res.error !== null) {
-      toast.error("There was an error")
+      toast.error("There was an error", { id: loading })
       console.error(res.error)
       return
     }
 
+    handleOpenChange(false)
     await wait(2000)
     toast.success(`Updated successfully!`, { id: loading })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/azure/members/set-assoc-number-dialog.tsx around
lines 35 - 41, The dialog is being closed before checking the mutation result;
move the call that closes/resets the dialog (handleOpenChange(false)) into the
success branch after verifying setAzureMemberNumber's response (res.error ===
null), and only then clear/reset any input state and dismiss the loading toast;
on error leave the dialog open, dismiss the loading toast and show the
toast.error (and console.error(res.error)) so the user can correct and retry.
Ensure you update the code paths around setAzureMemberNumber, the loading toast
variable, and handleOpenChange accordingly.
src/app/dashboard/(active)/azure/members/table.tsx (1)

10-27: ⚠️ Potential issue | 🟡 Minor

Don't sort the members prop in place.

When sociFilter is false, users points to the same array instance as members. sortByAssocNumber() then mutates that prop during render, which can leak reordered data back to the parent and make subsequent renders harder to reason about. Sort a copy instead.

Proposed fix
 function sortByAssocNumber(users: AzureMember[]) {
   if (!users || users.length === 0) return users
-  if (users.every((u) => !u.employeeId))
-    return users.sort((a, b) => (a.displayName ?? "").localeCompare(b.displayName ?? ""))
+  const sorted = [...users]
+  if (sorted.every((u) => !u.employeeId))
+    return sorted.sort((a, b) => (a.displayName ?? "").localeCompare(b.displayName ?? ""))
 
-  return users.sort((a, b) => {
+  return sorted.sort((a, b) => {
     if (a.employeeId && b.employeeId) {
       const aInt = parseInt(a.employeeId, 10)
       const bInt = parseInt(b.employeeId, 10)

Also applies to: 30-32

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/azure/members/table.tsx around lines 10 - 27,
sortByAssocNumber mutates the incoming array (which can be the members prop) by
calling .sort() in-place; make it operate on a shallow copy instead to avoid
mutating props — e.g. copy users at the start of sortByAssocNumber (or before
any .sort() call) and perform all sorting on that copy, then return the new
sorted array; update every occurrence of .sort() in this file (including the
other sort calls around the same logic) to act on a copied array rather than the
original users/members reference.
🧹 Nitpick comments (5)
src/app/dashboard/(active)/telegram/grants/grant-list.tsx (1)

35-35: Consider using () => {} instead of () => null for void callbacks.

While functionally equivalent (return value is ignored), () => {} better expresses intent for a void return type callback.

♻️ Proposed fix
-      <DeleteGrant userId={r.grant.userId} onDelete={() => null} />
+      <DeleteGrant userId={r.grant.userId} onDelete={() => {}} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/grants/grant-list.tsx at line 35,
Replace the no-op delete callback passed to DeleteGrant (prop onDelete) from "()
=> null" to an explicit empty function "() => {}" to better convey a void-return
intent; locate the DeleteGrant usage in grant-list.tsx (the JSX element
DeleteGrant with props userId and onDelete) and update the onDelete prop
accordingly.
src/app/dashboard/(active)/azure/members/page.tsx (1)

17-20: Suspense boundary may be unnecessary after this refactor.

Since getAzureMembers() is awaited at Line 10 before rendering, the Suspense boundary no longer serves its original purpose of showing a loading state during data fetch. The data is already resolved by the time AssocTable renders.

Consider either:

  1. Removing the Suspense wrapper if AssocTable has no lazy-loaded children
  2. Moving data fetching inside AssocTable as a suspended promise if you want to preserve the loading UX
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/azure/members/page.tsx around lines 17 - 20, The
Suspense wrapper around AssocTable is redundant because getAzureMembers() is
awaited before render; either remove the <Suspense>... </Suspense> wrapper in
page.tsx so ErrorBoundary directly wraps <AssocTable members={members} />, or if
you need suspended loading behavior move the data fetching into AssocTable
(e.g., call getAzureMembers from inside AssocTable and return a suspending
promise) and keep Suspense; update references to AssocTable and getAzureMembers
accordingly.
src/server/actions/users.ts (1)

22-24: Consider parallelizing independent queries for better performance.

These three queries only depend on user.id and can run concurrently.

⚡ Proposed optimization
-  const { roles, groupAdmin } = await trpc.tg.permissions.getRoles.query({ userId: user.id })
-  const { messages } = await trpc.tg.messages.getLastByUser.query({ userId: user.id, limit: 15 })
-  const audit = await trpc.tg.auditLog.getById.query({ targetId: user.id })
+  const [{ roles, groupAdmin }, { messages }, audit] = await Promise.all([
+    trpc.tg.permissions.getRoles.query({ userId: user.id }),
+    trpc.tg.messages.getLastByUser.query({ userId: user.id, limit: 15 }),
+    trpc.tg.auditLog.getById.query({ targetId: user.id }),
+  ])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/actions/users.ts` around lines 22 - 24, The three independent TRPC
calls (trpc.tg.permissions.getRoles.query, trpc.tg.messages.getLastByUser.query,
trpc.tg.auditLog.getById.query) should be executed in parallel rather than
sequentially; replace the three awaits with a single Promise.all call that runs
the three queries concurrently and then destructure the returned results into
roles/groupAdmin, messages, and audit so the rest of the code uses the same
variables.
src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx (1)

41-41: Remove commented-out code.

This leftover mutation code should be deleted.

🧹 Proposed fix
-  // const submitMutation = useMutation(trpc.tg.permissions.addGroup.mutationOptions())
-
   async function submit() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/user-details/new-group-admin.tsx at line
41, Remove the leftover commented-out mutation declaration—delete the line
containing "const submitMutation =
useMutation(trpc.tg.permissions.addGroup.mutationOptions())" in
new-group-admin.tsx so the file no longer contains the commented code for
submitMutation/useMutation.
src/app/dashboard/(active)/telegram/user-details/page.tsx (1)

88-88: Fallback to 0 may cause unintended filtering behavior.

If g is null, the fallback ?? 0 produces 0 in the alreadyIn array. If 0 happens to be a valid group ID (or matches a searched group), it would be incorrectly filtered out in NewGroupAdmin.

Consider filtering out null entries first:

♻️ Proposed fix
-              alreadyIn={data.groupAdmin.map((g) => g?.group.id ?? 0) ?? []}
+              alreadyIn={data.groupAdmin.filter((g) => g !== null).map((g) => g.group.id)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/user-details/page.tsx at line 88, The
current alreadyIn value maps data.groupAdmin with g?.group.id ?? 0 which inserts
0 for null entries and may incorrectly exclude real group id 0; update this to
first filter out null/undefined entries (e.g., filter(g => g?.group?.id !=
null)) and then map to g.group.id so alreadyIn only contains real IDs; apply
this change where alreadyIn is passed (the alreadyIn prop in the
NewGroupAdmin/parent call) to avoid introducing a 0 placeholder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(auth)/onboarding/no-role/page.tsx:
- Line 14: The code destructures roles directly from getUserRoles(...) which can
return null; change it to first await and store the result (e.g., call
getUserRoles(...) into a variable like userRoles or rolesResult), then access
roles null-safely (use optional chaining and a fallback, e.g., userRoles?.roles
?? [] or handle the null case explicitly) before using the roles variable so
page.tsx no longer throws when getUserRoles returns null.

In `@src/app/dashboard/`(active)/account/telegram.tsx:
- Line 8: getUserRoles(data.user.telegramId) can return null, so avoid
destructuring directly; call getUserRoles(...) into a variable (e.g.,
userRolesResult) and then safely extract roles with a fallback (e.g., const
roles = userRolesResult?.roles ?? []), or handle the null case by returning
early or rendering a fallback UI. Update the code around the current
destructuring to use that safe access pattern and ensure downstream code expects
an array.
- Line 13: The JSX conditional rendering currently uses the truthiness of
roles?.length which can render a literal 0 when roles is an empty array; change
the condition to an explicit boolean check like roles?.length > 0 so the span
only renders when there is at least one role—update the expression where roles
is referenced in the JSX (the line with {roles?.length && <span ...>(roles:
{roles.join(", ")})</span>}) to use roles?.length > 0 while keeping the
roles.join(", ") usage intact.

In `@src/app/dashboard/`(active)/azure/members/create-assoc-member.tsx:
- Around line 37-45: handleSubmit currently awaits createAzureMember and only
checks res.error, but createAzureMember can throw and cause unhandled runtime
exceptions; wrap the await call in a try/catch inside handleSubmit, call
createAzureMember({ assocNumber: parseInt(assocNumber,10), firstName, lastName,
sendEmailTo: sendTo }) inside the try, then handle thrown errors in the catch
(e.g., set a local error state or call the existing error handling path and/or
processLogger) and also keep the existing res.error handling for server-returned
errors so all failure modes are covered.

In `@src/app/dashboard/`(active)/azure/members/set-assoc-number-dialog.tsx:
- Around line 1-2: This file uses client-only hooks (useRouter, useState,
useTransition) so add the "use client" directive as the very first line of
src/app/dashboard/(active)/azure/members/set-assoc-number-dialog.tsx (before any
imports) to make the module a client component; ensure the string literal is
exactly "use client" and placed above the existing imports so useRouter,
useState, and useTransition work correctly.

In `@src/app/dashboard/`(active)/telegram/grants/delete-grant.tsx:
- Line 35: Fix the typo in the toast error message: replace "You don't have
enought permission" with "You don't have enough permission" (locate the string
in src/app/dashboard/(active)/telegram/grants/delete-grant.tsx where the code
checks else if (error === "UNAUTHORIZED") and calls toast.error).

In `@src/app/dashboard/`(active)/telegram/grants/page.tsx:
- Line 1: The page component incorrectly includes the "use server" directive;
remove that top-level "use server" string from the page component file
(page.tsx) so the component remains a default Server Component, and if you need
Server Actions move them into a separate exported function annotated with "use
server" instead of placing the directive in the page component itself.

In `@src/app/dashboard/`(active)/telegram/groups/group-row.tsx:
- Around line 14-18: The code currently calls setGroupHide(r.telegramId,
!r.hide) and on rejection sets ok to false but still proceeds to call
toast.success and router.refresh; change the flow in the handler around
setGroupHide (the ok variable) to return early when ok is falsy: after const ok
= await setGroupHide(...).catch(() => false) check if (!ok) { toast.error("The
field cannot be modified"); return; } so that toast.success("Hide option
toggled!") and router.refresh() only run on a successful update.
- Around line 59-60: The edit button is currently hidden when r.link is falsy
due to the className check {!r.link ? "hidden" : ""}, preventing toggling
hide/unhide for rows without a link; remove that conditional so the <Button> in
group-row.tsx always renders (i.e., delete the {!r.link ? "hidden" : ""} logic)
and keep the onClick handler toggleHide unchanged so rows without r.link can
still toggle the hide flag.

In `@src/app/dashboard/`(active)/telegram/groups/page.tsx:
- Line 1: Remove the incorrect "use server" directive at the top of this page
component so Next.js treats the default export as a Server Component (the app
router default) rather than a Server Action; simply delete the "use server" line
and ensure any server-only functions remain normal server-side code (no Server
Action directive needed) in the exported page component.

In `@src/app/dashboard/`(active)/telegram/groups/search-input.tsx:
- Around line 11-13: The input's local state (value) is only set once from
useSearchParams and doesn't update when the URL query "q" changes; remove the
unused _search and add a useEffect that reads useSearchParams().get("q") and
calls setValue(...) whenever that param changes so the textbox stays in sync
with external navigation, keeping the existing value/setValue wiring and
ensuring initial mount behavior remains the same.

In `@src/app/dashboard/`(active)/telegram/user-details/delete-group-admin.tsx:
- Around line 22-41: Prevent duplicate deletes and avoid closing the dialog on
failure by adding an "in-flight" flag: introduce a boolean state (e.g.,
isDeleting) in the DeleteGroupAdmin component and early-return from
deleteGroupAdmin if isDeleting is true; set isDeleting = true right before
calling delGroupAdmin(userId, chatId, removerId), setOpen(false) only on
successful completion (inside the try after await and before calling onDelete),
and in the catch reset isDeleting = false (so the user can retry) while keeping
the dialog open; also use isDeleting to disable the delete button/UI while the
request is in progress.

In `@src/app/dashboard/`(active)/telegram/user-details/page.tsx:
- Line 21: The current useActionState call captures `username` from component
state instead of using the FormData provided by submissions; update the
`useActionState` invocation (the tuple [data, action, pending]) so its action
callback signature is async (prev: Data | null, formData: FormData) => { const
username = formData.get("username") as string; return username ?
searchUser(username) : null } and keep the initial state as null, and modify the
reset handler to directly clear the input value (remove the call to action()
with no arguments) so the form submission always reads the username from the
FormData rather than the closure over component state.

In `@src/server/trpc/index.tsx`:
- Around line 9-15: The static trpc client (export const trpc =
createTRPCClient<AppRouter>...) loses incoming request headers and breaks auth;
replace it with an async getTrpcClient() that uses Next.js's headers() API to
collect incoming headers and pass them into the httpBatchLink's headers option
(preserving cookies/authorization) while still using SuperJSON and AppRouter;
update all server-action callers to use "const trpc = await getTrpcClient()"
instead of importing the static trpc.

---

Outside diff comments:
In `@src/app/dashboard/`(active)/azure/members/set-assoc-number-dialog.tsx:
- Around line 35-41: The dialog is being closed before checking the mutation
result; move the call that closes/resets the dialog (handleOpenChange(false))
into the success branch after verifying setAzureMemberNumber's response
(res.error === null), and only then clear/reset any input state and dismiss the
loading toast; on error leave the dialog open, dismiss the loading toast and
show the toast.error (and console.error(res.error)) so the user can correct and
retry. Ensure you update the code paths around setAzureMemberNumber, the loading
toast variable, and handleOpenChange accordingly.

In `@src/app/dashboard/`(active)/azure/members/table.tsx:
- Around line 10-27: sortByAssocNumber mutates the incoming array (which can be
the members prop) by calling .sort() in-place; make it operate on a shallow copy
instead to avoid mutating props — e.g. copy users at the start of
sortByAssocNumber (or before any .sort() call) and perform all sorting on that
copy, then return the new sorted array; update every occurrence of .sort() in
this file (including the other sort calls around the same logic) to act on a
copied array rather than the original users/members reference.

In `@src/app/dashboard/`(active)/telegram/grants/new-grant.tsx:
- Around line 79-94: The createGrant(...) server-action call may throw and is
currently unguarded; wrap the await createGrant({...}) call in a try/catch
around the block that handles res so that thrown errors are caught and reported
to the user instead of causing an unhandled rejection. Specifically, place the
await createGrant call (and the subsequent res.error checks and success
handling) inside a try block, catch any error in the catch block, log or toast a
generic/error message (e.g., toast.error with the exception message) and ensure
router.refresh() and success toast only run in the happy path; reference the
createGrant invocation and the existing res/error branches when updating the
code.

In `@src/components/user-select.tsx`:
- Around line 25-31: In search(), validate the id before calling getUserInfo:
check that the id string is non-empty and parses to a finite integer (e.g., via
/^\d+$/ test or Number.isInteger(Number(id))) when searchType === "id"; if
validation fails, call toast.error with a clear message and return early so you
never pass NaN to getUserInfo; keep existing flow for username branch and
continue to call setUser(user) and onUser(user) only after a successful
getUserInfo or searchUserInfo result.

---

Nitpick comments:
In `@src/app/dashboard/`(active)/azure/members/page.tsx:
- Around line 17-20: The Suspense wrapper around AssocTable is redundant because
getAzureMembers() is awaited before render; either remove the <Suspense>...
</Suspense> wrapper in page.tsx so ErrorBoundary directly wraps <AssocTable
members={members} />, or if you need suspended loading behavior move the data
fetching into AssocTable (e.g., call getAzureMembers from inside AssocTable and
return a suspending promise) and keep Suspense; update references to AssocTable
and getAzureMembers accordingly.

In `@src/app/dashboard/`(active)/telegram/grants/grant-list.tsx:
- Line 35: Replace the no-op delete callback passed to DeleteGrant (prop
onDelete) from "() => null" to an explicit empty function "() => {}" to better
convey a void-return intent; locate the DeleteGrant usage in grant-list.tsx (the
JSX element DeleteGrant with props userId and onDelete) and update the onDelete
prop accordingly.

In `@src/app/dashboard/`(active)/telegram/user-details/new-group-admin.tsx:
- Line 41: Remove the leftover commented-out mutation declaration—delete the
line containing "const submitMutation =
useMutation(trpc.tg.permissions.addGroup.mutationOptions())" in
new-group-admin.tsx so the file no longer contains the commented code for
submitMutation/useMutation.

In `@src/app/dashboard/`(active)/telegram/user-details/page.tsx:
- Line 88: The current alreadyIn value maps data.groupAdmin with g?.group.id ??
0 which inserts 0 for null entries and may incorrectly exclude real group id 0;
update this to first filter out null/undefined entries (e.g., filter(g =>
g?.group?.id != null)) and then map to g.group.id so alreadyIn only contains
real IDs; apply this change where alreadyIn is passed (the alreadyIn prop in the
NewGroupAdmin/parent call) to avoid introducing a 0 placeholder.

In `@src/server/actions/users.ts`:
- Around line 22-24: The three independent TRPC calls
(trpc.tg.permissions.getRoles.query, trpc.tg.messages.getLastByUser.query,
trpc.tg.auditLog.getById.query) should be executed in parallel rather than
sequentially; replace the three awaits with a single Promise.all call that runs
the three queries concurrently and then destructure the returned results into
roles/groupAdmin, messages, and audit so the rest of the code uses the same
variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cf25644-e3ad-4f3a-b85f-eee09e7fe9e8

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8cc16 and 9e476e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (42)
  • package.json
  • src/app/(auth)/onboarding/no-role/page.tsx
  • src/app/dashboard/(active)/account/telegram.tsx
  • src/app/dashboard/(active)/azure/members/columns.tsx
  • src/app/dashboard/(active)/azure/members/create-assoc-member.tsx
  • src/app/dashboard/(active)/azure/members/page.tsx
  • src/app/dashboard/(active)/azure/members/set-assoc-number-dialog.tsx
  • src/app/dashboard/(active)/azure/members/table.tsx
  • src/app/dashboard/(active)/telegram/grants/delete-grant.tsx
  • src/app/dashboard/(active)/telegram/grants/grant-list.tsx
  • src/app/dashboard/(active)/telegram/grants/new-grant.tsx
  • src/app/dashboard/(active)/telegram/grants/page.tsx
  • src/app/dashboard/(active)/telegram/groups/group-row.tsx
  • src/app/dashboard/(active)/telegram/groups/page.tsx
  • src/app/dashboard/(active)/telegram/groups/search-input.tsx
  • src/app/dashboard/(active)/telegram/user-details/add-role.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-audit-log.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-group-admin.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-message.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-user-grant.tsx
  • src/app/dashboard/(active)/telegram/user-details/card-user-info.tsx
  • src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx
  • src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx
  • src/app/dashboard/(active)/telegram/user-details/page.tsx
  • src/app/dashboard/(active)/telegram/user-details/remove-role.tsx
  • src/app/dashboard/(active)/telegram/user-list/page.tsx
  • src/app/dashboard/layout.tsx
  • src/app/layout.tsx
  • src/app/testtrpc/page.tsx
  • src/components/data-table.tsx
  • src/components/user-select.tsx
  • src/lib/trpc/client.tsx
  • src/lib/trpc/query-client.ts
  • src/lib/trpc/server.tsx
  • src/lib/utils/telegram.ts
  • src/server/actions/azure.ts
  • src/server/actions/grants.ts
  • src/server/actions/groups.ts
  • src/server/actions/test.ts
  • src/server/actions/users.ts
  • src/server/trpc/index.tsx
  • src/server/trpc/types.tsx
💤 Files with no reviewable changes (4)
  • package.json
  • src/lib/trpc/query-client.ts
  • src/lib/trpc/server.tsx
  • src/lib/trpc/client.tsx

Comment thread src/app/(auth)/onboarding/no-role/page.tsx
Comment thread src/app/dashboard/(active)/account/telegram.tsx
Comment thread src/app/dashboard/(active)/account/telegram.tsx Outdated
Comment thread src/app/dashboard/(active)/azure/members/create-assoc-member.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/groups/page.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/groups/search-input.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/user-details/page.tsx Outdated
Comment thread src/server/trpc/index.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/middleware.ts`:
- Line 7: The matcher currently uses a template literal with an imported
variable (`matcher: [\`${AUTH_PATH}/:path*\`]`) which Next.js cannot statically
analyze; update the matcher in middleware.ts to use a static string literal or
an exported const instead so Next.js can analyze it at build time (e.g., replace
the template literal with a hardcoded string like "/auth/:path*" or export a
top-level constant and reference that constant in the matcher), ensuring the
matcher field is a plain string the framework can statically evaluate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13f83c2a-f41e-4882-8ec4-7ae6c792395d

📥 Commits

Reviewing files that changed from the base of the PR and between 9e476e9 and b25a5ce.

📒 Files selected for processing (2)
  • src/middleware.ts
  • src/server/trpc/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/trpc/index.tsx

Comment thread src/middleware.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/dashboard/`(active)/telegram/groups/page.tsx:
- Around line 11-12: The code always awaits trpc.tg.groups.getAll.query() even
when a search query q is present, causing an unnecessary extra round-trip;
change the logic so you only call trpc.tg.groups.getAll.query() when q is falsy
and call trpc.tg.groups.search.query({ limit: 20, query: q, showHidden: true })
only when q is truthy, then assign rows accordingly (use the existing variables
all, rows, q and the trpc methods trpc.tg.groups.getAll.query and
trpc.tg.groups.search.query to locate and update the conditional).
- Around line 8-12: searchParams.q can be string | string[] | undefined;
normalize it in TgGroups before using it: await the searchParams, coerce q to a
single string (e.g., if Array take the first element), trim and convert empty
strings to undefined, then use that normalized string when calling
trpc.tg.groups.search.query({ query: q }). Update references in TgGroups (the
const { q } = await searchParams assignment and the subsequent
trpc.tg.groups.search.query call) so only the normalized single string or
undefined is passed.

In `@src/app/dashboard/`(active)/telegram/user-list/page.tsx:
- Around line 15-17: The Count display currently shows a blank when data.users
is missing; update the JSX in page.tsx where Count is rendered to use an
explicit numeric fallback (e.g., replace data.users?.length with
(data.users?.length ?? 0) or similar) so it always renders 0 when users is
undefined/null; ensure you update the expression inside the <span> that
references data.users to use the nullish-coalescing fallback.
- Around line 8-9: The server-rendered TgUsers component calls
trpc.tg.users.getAll.query but doesn't declare a caching/render policy; pick and
set an explicit Next.js cache policy at the top of this file (e.g., export const
revalidate = 0 for no-cache/SSR, export const revalidate = <seconds> for ISR, or
export const dynamic = "force-dynamic" for always-dynamic) so data freshness is
explicit for TgUsers, and ensure that any mutation code that updates user
roles/permissions calls the appropriate invalidation (revalidatePath or
revalidateTag) matching your chosen strategy to refresh this list after changes;
locate symbols TgUsers and trpc.tg.users.getAll.query to apply the change and
update mutation handlers that change users to trigger revalidation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 525a9855-3034-4dce-8590-5aa4a7cc1887

📥 Commits

Reviewing files that changed from the base of the PR and between b25a5ce and 99ec56d.

📒 Files selected for processing (2)
  • src/app/dashboard/(active)/telegram/groups/page.tsx
  • src/app/dashboard/(active)/telegram/user-list/page.tsx

Comment thread src/app/dashboard/(active)/telegram/groups/page.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/groups/page.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/user-list/page.tsx
Comment thread src/app/dashboard/(active)/telegram/user-list/page.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
src/app/dashboard/(active)/telegram/groups/page.tsx (2)

7-11: ⚠️ Potential issue | 🟡 Minor

Still unresolved: normalize q before calling search.query().

searchParams.q is still treated as a scalar string. A request like ?q=a&q=b can send an array into trpc.tg.groups.search.query(...). Coerce it to one string, trim it, and treat empty values as undefined before branching.

Suggested fix
-export default async function TgGroups({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
-  const { q } = await searchParams
+export default async function TgGroups({
+  searchParams,
+}: {
+  searchParams: Promise<{ q?: string | string[] }>
+}) {
+  const { q: rawQ } = await searchParams
+  const q = (Array.isArray(rawQ) ? rawQ[0] : rawQ)?.trim() || undefined
Next.js official App Router docs: what is the `searchParams` page prop type, and can repeated query string keys be exposed as `string[]`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/groups/page.tsx around lines 7 - 11,
Normalize the incoming search param before using it: in TgGroups, coerce
searchParams.q to a single string (if it's an array, pick the first element),
trim() it, and convert empty strings to undefined, then use that normalized
value for the q check and when calling trpc.tg.groups.search.query({ limit: 20,
query: qNormalized, showHidden: true }); ensure all references to q in this
function use the normalized variable.

10-11: ⚠️ Potential issue | 🟠 Major

Still unresolved: avoid getAll() on filtered requests.

The page still waits for trpc.tg.groups.getAll.query() even when q is present and then throws that result away. Every search now does two server calls instead of one.

Suggested fix
-  const all = await trpc.tg.groups.getAll.query()
-  const rows = q ? (await trpc.tg.groups.search.query({ limit: 20, query: q, showHidden: true })).groups : all
+  const rows = q
+    ? (await trpc.tg.groups.search.query({ limit: 20, query: q, showHidden: true })).groups
+    : await trpc.tg.groups.getAll.query()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dashboard/`(active)/telegram/groups/page.tsx around lines 10 - 11,
The code always awaits trpc.tg.groups.getAll.query() then discards it when q is
set, causing unnecessary double requests; change the logic so you only call
trpc.tg.groups.getAll.query() when q is falsy and only call
trpc.tg.groups.search.query(...) when q is truthy (e.g., use an if/else around
the awaits or await the appropriate call into rows directly), referencing
trpc.tg.groups.getAll.query, trpc.tg.groups.search.query, the q variable and the
rows assignment to locate the code to update.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/dashboard/`(active)/account/telegram.tsx:
- Around line 13-14: The span currently renders "@{data.user.telegramUsername}"
which outputs "@undefined" when data.user.telegramUsername is missing; update
the JSX so it only prefixes with "@" when data.user.telegramUsername is truthy
and otherwise either show a fallback label (e.g., "Telegram (no username)" or
the numeric ID if available) or omit the username span entirely; target the span
that references data.user.telegramUsername and adjust the conditional rendering
(and any related variable checks like roles and length) to avoid rendering
"@undefined".

In `@src/app/dashboard/`(active)/telegram/grants/delete-grant.tsx:
- Around line 34-40: The NOT_FOUND branch currently only shows a toast which
leaves stale UI; update the handler in delete-grant.tsx so that when error ===
"NOT_FOUND" you perform the same UI refresh/removal steps as the success branch:
show the info toast, call router.refresh(), and invoke onDelete() (same sequence
used in the success branch) so list and card components are updated/removed
consistently.
- Around line 30-43: The interrupt function should guard the await
interruptGrant call with a try/catch/finally: inside try, call interruptGrant({
userId, interruptedById: removerId, sendTgLog: true }) and keep the existing
error branch logic (NOT_FOUND, UNAUTHORIZED, INTERNAL_SERVER_ERROR) and success
branch (toast.success + router.refresh + onDelete); in catch, show a generic
toast.error with the caught error message (or a fallback) to handle
transport/unexpected rejections; in finally, ensure handleOpenChange(false)
always runs so the modal closes; keep the initial removerId check and return
early as-is.

In `@src/server/actions/users.ts`:
- Around line 37-50: The four exported functions (addGroupAdmin, delGroupAdmin,
addUserRole, delUserRole) currently accept adderId/removerId from callers which
allows spoofing; remove those actor params and instead derive the actor from the
authenticated session/context inside each function (e.g., use session.user.id or
the service's currentUser/context user), then call trpc.tg.permissions.*.mutate
with that resolved actorId (pass actorId as adderId/removerId) so audit data is
authoritative; update any callers and types that passed adderId/removerId to
match the new signatures.
- Around line 22-34: The audits mapping is storing the full trpc.tg.users.get
payload in audits[n].admin and calling trpc.tg.users.get for every audit row;
change this to first collect unique adminIds from the audit array, fetch each
admin once (e.g., call trpc.tg.users.get for each unique id or use a batched
helper), build a map adminId -> TgUser | null, then rebuild audits by spreading
audit and setting admin: adminMap[audit.adminId]?.user (i.e., unwrap the .user
field) so audits store TgUser | null and avoid duplicate backend round-trips;
update the code around the audits Promise.all block to use the deduped lookup
and unwrapped admin.

---

Duplicate comments:
In `@src/app/dashboard/`(active)/telegram/groups/page.tsx:
- Around line 7-11: Normalize the incoming search param before using it: in
TgGroups, coerce searchParams.q to a single string (if it's an array, pick the
first element), trim() it, and convert empty strings to undefined, then use that
normalized value for the q check and when calling trpc.tg.groups.search.query({
limit: 20, query: qNormalized, showHidden: true }); ensure all references to q
in this function use the normalized variable.
- Around line 10-11: The code always awaits trpc.tg.groups.getAll.query() then
discards it when q is set, causing unnecessary double requests; change the logic
so you only call trpc.tg.groups.getAll.query() when q is falsy and only call
trpc.tg.groups.search.query(...) when q is truthy (e.g., use an if/else around
the awaits or await the appropriate call into rows directly), referencing
trpc.tg.groups.getAll.query, trpc.tg.groups.search.query, the q variable and the
rows assignment to locate the code to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dadef47c-26a3-4e22-b042-da596965d035

📥 Commits

Reviewing files that changed from the base of the PR and between 99ec56d and e3a4eaa.

📒 Files selected for processing (11)
  • src/app/dashboard/(active)/account/telegram.tsx
  • src/app/dashboard/(active)/azure/members/create-assoc-member.tsx
  • src/app/dashboard/(active)/azure/members/set-assoc-number-dialog.tsx
  • src/app/dashboard/(active)/telegram/grants/delete-grant.tsx
  • src/app/dashboard/(active)/telegram/grants/page.tsx
  • src/app/dashboard/(active)/telegram/groups/group-row.tsx
  • src/app/dashboard/(active)/telegram/groups/page.tsx
  • src/app/dashboard/(active)/telegram/groups/search-input.tsx
  • src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx
  • src/middleware.ts
  • src/server/actions/users.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/app/dashboard/(active)/telegram/grants/page.tsx
  • src/middleware.ts
  • src/app/dashboard/(active)/telegram/groups/group-row.tsx
  • src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx
  • src/app/dashboard/(active)/telegram/groups/search-input.tsx
  • src/app/dashboard/(active)/azure/members/create-assoc-member.tsx
  • src/app/dashboard/(active)/azure/members/set-assoc-number-dialog.tsx

Comment thread src/app/dashboard/(active)/account/telegram.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/grants/delete-grant.tsx Outdated
Comment thread src/app/dashboard/(active)/telegram/grants/delete-grant.tsx Outdated
Comment thread src/server/actions/users.ts
Comment thread src/server/actions/users.ts Outdated
@toto04 toto04 merged commit 5f4710a into main Apr 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants